// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.

using System.Reflection;
using System.Runtime.CompilerServices;
using Stride.Core.Assets;
using Stride.Core.Serialization.Contents;

namespace Stride.Core.Serialization;

/// <summary>
/// Manage object reference information externally, not stored in the object but in a separate <see cref="AttachedReference"/> object.
/// </summary>
// TODO: this should be ideally moved back in Core.Design, but there are a few runtime usages to clean first
public static class AttachedReferenceManager
{
    private static readonly object[] EmptyObjectArray = [];
    private static readonly Dictionary<Type, ConstructorInfo> EmptyCtorCache = [];
    private static readonly ConditionalWeakTable<object, AttachedReference> AttachedReferences = [];

    /// <summary>
    /// Gets the URL of a given object.
    /// </summary>
    /// <param name="obj">The object.</param>
    /// <returns>The URL.</returns>
    public static string? GetUrl(object obj)
    {
        return AttachedReferences.TryGetValue(obj, out var attachedReference) ? attachedReference.Url : null;
    }

    /// <summary>
    /// Sets the URL of a given object.
    /// </summary>
    /// <param name="obj">The object.</param>
    /// <param name="url">The URL.</param>
    public static void SetUrl(object obj, string url)
    {
        var attachedReference = AttachedReferences.GetValue(obj, x => new AttachedReference());
        attachedReference.Url = url;
    }

    /// <summary>
    /// Gets the reference info of attached to a given object, if it exists.
    /// </summary>
    /// <param name="obj">The object for which to get the attached reference. Can be null, in this case this method returns null.</param>
    /// <returns>The <see cref="AttachedReference"/> attached to the given object if available, <c>null</c> otherwise.</returns>
    public static AttachedReference? GetAttachedReference(object? obj)
    {
        if (obj == null)
            return null;

        AttachedReferences.TryGetValue(obj, out var attachedReference);
        return attachedReference;
    }

    /// <summary>
    /// Gets or creates the object reference info of a given object.
    /// </summary>
    /// <param name="obj">The object.</param>
    public static AttachedReference GetOrCreateAttachedReference(object obj)
    {
        return AttachedReferences.GetValue(obj, _ => new AttachedReference());
    }

    /// <summary>
    /// Creates a proxy object with <see cref="AttachedReference" /> designing it as a proxy with a given id and location (that can be used with <see cref="ContentManager" />). This allows to construct and save object references without actually loading them.
    /// </summary>
    /// <param name="reference">The content reference.</param>
    /// <returns>T.</returns>
    /// <exception cref="System.ArgumentNullException">reference</exception>
    public static T CreateProxyObject<T>(IReference reference) where T : class, new()
    {
        ArgumentNullException.ThrowIfNull(reference);
        return CreateProxyObject<T>(reference.Id, reference.Location);
    }

    /// <summary>
    /// Creates a proxy object with <see cref="AttachedReference"/> designing it as a proxy with a given id and location (that can be used with <see cref="ContentManager"/>). This allows to construct and save object references without actually loading them.
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <param name="location">The location.</param>
    public static T CreateProxyObject<T>(AssetId id, string location) where T : class, new()
    {
        var result = new T();
        InitializeProxyObject(result, id, location);

        return result;
    }

    /// <summary>
    /// Creates a proxy object with <see cref="AttachedReference"/> designing it as a proxy with a given id and location (that can be used with <see cref="ContentManager"/>). This allows to construct and save object references without actually loading them.
    /// </summary>
    /// <param name="type">The type.</param>
    /// <param name="id">The identifier.</param>
    /// <param name="location">The location.</param>
    public static object CreateProxyObject(Type type, AssetId id, string location)
    {
        ConstructorInfo? emptyCtor;
        lock (EmptyCtorCache)
        {
            if (!EmptyCtorCache.TryGetValue(type, out emptyCtor))
            {
                foreach (var ctor in type.GetTypeInfo().DeclaredConstructors)
                {
                    if (!ctor.IsStatic && ctor.GetParameters().Length == 0)
                    {
                        emptyCtor = ctor;
                        break;
                    }
                }
                if (emptyCtor == null)
                {
                    throw new InvalidOperationException($"Type {type} has no empty ctor");
                }
                EmptyCtorCache.Add(type, emptyCtor);
            }
        }
        var result = emptyCtor.Invoke(EmptyObjectArray);
        InitializeProxyObject(result, id, location);
        return result;
    }
    private static void InitializeProxyObject(object proxyObject, AssetId id, string location)
    {
        var attachedReference = GetOrCreateAttachedReference(proxyObject);
        attachedReference.Id = id;
        attachedReference.Url = location;
        attachedReference.IsProxy = true;
    }
}
